///////////////////////////////////////////////////////////////////////////////
//
// Licensed under the MIT License
//
// Copyright (c) 2010 Chris Colinsky
// All Rights Reserved
//
// version	0.5
// http://code.google.com/p/sugarcookie/
// http://code.google.com/p/sugarcookie/wiki/License
//
////////////////////////////////////////////////////////////////////////////////

package sugarcookie.media {
	import com.akamai.AkamaiConnection;
	import com.akamai.events.AkamaiErrorEvent;
	import com.akamai.events.AkamaiNotificationEvent;
	
	import flash.events.*;
	import flash.media.SoundTransform;
	import flash.media.Video;
	import flash.net.NetConnection;
	import flash.net.NetStream;
	
	import mx.utils.UIDUtil;
	
	import sugarcookie.display.SpriteBase;
	import sugarcookie.error.ApplicationError;
	import sugarcookie.media.config.UIVideoConfiguration;
	import sugarcookie.media.VideoSourceConstants;
	import sugarcookie.utils.IObservable;
	import sugarcookie.utils.IObserver;
	import sugarcookie.utils.NumberUtil;
	import sugarcookie.utils.Observable;
	
	/**
	 * Proxy component for the combination of the NetConnection, NetStream and Video classes.
	 */
	public class UIVideo extends SpriteBase implements IVideoSource {
		private var _isPlaying:Boolean = false;
		private var _isPaused:Boolean = false;
		private var _isSeeking:Boolean = false;
		private var _config:UIVideoConfiguration;
		private var _ak:AkamaiConnection;
        private var _nc:NetConnection=null;
        private var _ns:NetStream=null;
        private var _src:String;
        private var _vol:Number;
        private var _lt_buffer:Number;
        private var _ht_buffer:Number;
        private var _isManualClose:Boolean = false;
        private var _videoDur:Number = -1;
        private var _video:Video;
		private var _buffering:Boolean = true;
		private var _notifier:Observable;
		
		public function UIVideo(config:UIVideoConfiguration,props:Object=null) {
			super(props);
			_config = config;
			_vol = config.volume;
			_lt_buffer = config.lowToleranceBuffer;
			_ht_buffer = config.highToleranceBuffer;

			init(config.videoWidth,config.videoHeight);
		}
		
		//Observable methods
		final public function addObserver(observer:IObserver,notificationType:String="all"):Boolean {
			return _notifier.addObserver(observer,notificationType);
		}
		
		final public function removeObserver(observer:IObserver,notificationType:String="all"):Boolean {
			return _notifier.removeObserver(observer,notificationType);
		}
		
		final public function clearObservers():void {
			_notifier.clearObservers();
		}
		
		final public function countObservers():Number {
			return _notifier.countObservers();
		}
		
		final public function get useLogging():Boolean { return _notifier.useLogging; }
		final public function set useLogging(value:Boolean):void { _notifier.useLogging = value; }
			
		final public function get videoDur():Number { return _videoDur; }
		
		final public function get currTime():Number {
			var ct:Number = (_ns==null) ? 0 : _ns.time;
			return ct;
		}
		
		final public function get loadedPerc():Number {
			var ld:Number = (_ns==null) ? 0 : (_ns.bytesLoaded / _ns.bytesTotal);
			return ld;
		}
		
		final public function get isStreaming():Boolean { return _config.hostname?true:false; }
		
		final public function get isPlaying():Boolean { return _isPlaying; }
		
		final public function get isPaused():Boolean { return _isPaused; }
		
		final public function get smoothing():Boolean { return _video.smoothing; }
		final public function set smoothing(b:Boolean):void { _video.smoothing = b; }
		
		final public function get volume():Number { return _vol; }
		final public function set volume(value:Number):void { _ns.soundTransform = new SoundTransform(_vol = value); }
		
		final public function get source():String { return _src; }
		final public function set source(value:String):void { _src = value; }
		
		final public function set buffer_highTolerance(_htb:int):void { _ht_buffer = _htb; }
		final public function set buffer_lowTolerance(_ltb:int):void { _lt_buffer = _ltb; }
		
		final public function get buffering():Boolean { return _buffering; }
		
		final public function get UID():String {
			return UIDUtil.getUID(_video);
		}
		
		final public function connect():void {
			_log.debug("UIVideo connect");
			
			if (_config.useAkamaiConnection) {
				_ak = new AkamaiConnection();
				_ak.addEventListener(AkamaiNotificationEvent.CONNECTED,connected);
				_ak.addEventListener(AkamaiErrorEvent.ERROR,connectError);
				_ak.connect(_config.hostname);
			} else {
				_nc = new NetConnection();
				_nc.client = this;
				_nc.objectEncoding = 0;
				_nc.addEventListener(NetStatusEvent.NET_STATUS, netStatusHandler);
				_nc.addEventListener(SecurityErrorEvent.SECURITY_ERROR, securityErrorHandler);
				_nc.addEventListener(IOErrorEvent.IO_ERROR, ioErrorHandler);
				_nc.addEventListener(AsyncErrorEvent.ASYNC_ERROR, asyncErrorHandler);
				_nc.connect(_config.hostname);
			}
		}
		
		final public function play(start:int=0,len:int=-1,reset:int=1):void {
			_log.debug("play() - "+_src);
			if (_src=="") throw new ApplicationError("No source has been set for UIVideo to play.");
			
			addChild(_video);
			
			_isPlaying = true;
			_isPaused = false;
			_ns.soundTransform = new SoundTransform(_vol);
			_ns.play(_config.path+_src,start,len,reset);
			_video.attachNetStream(_ns);
		}
		
		final public function pause():void {
			_isPlaying = false;
			_isPaused = true;
			_ns.pause();
			if (_config.hostname==null) removeEventListener(Event.ENTER_FRAME,listenForEnd);
		}
		
		final public function resume():void {
			_isPlaying = true;
			_isPaused = false;
			_ns.resume();
			if (_config.hostname==null) addEventListener(Event.ENTER_FRAME,listenForEnd);
		}
		
		final public function clear():void {
			_video.clear();
		}
		
		final public function togglePause():void {
			_isPlaying = !isPlaying;
			_isPaused = !_isPaused;
			_ns.togglePause();
			if (_config.hostname==null && _isPlaying) addEventListener(Event.ENTER_FRAME,listenForEnd);
			else if (_config.hostname==null && !_isPlaying) removeEventListener(Event.ENTER_FRAME,listenForEnd);
		}
		
		final public function close():void {
			_log.debug("close()");
			if (_ns==null) {
				_log.warn("netStream was never defined");
				return;
			}
			removeEventListener(Event.ENTER_FRAME,progressHandler);
			removeEventListener(Event.ENTER_FRAME,listenForEnd);
			removeEventListener(Event.ENTER_FRAME,notifyBufferingStatus);
			if (numChildren==1) removeChild(_video);
			_isManualClose = true;
			_isPlaying = false;
			_isPaused = false;
			_ns.close();
			clear();
		}
		
		final public function seek(_offset:Number):void {
			_ns.seek(_offset);
		}
		
		final public function get bufferPercent():Number {
			return Number((_ns.bufferLength/_ns.bufferTime).toFixed(2));
		}
		
		private function notifyObservers(type:String,data:Object=null):void {
			_notifier.setChanged();
			_notifier.notifyObservers(type, data);
		}
		
		private function init(videoWidth:int,videoHeight:int):void {
			_video = new Video();
			_video.width = videoWidth;
			_video.height = videoHeight;
			_video.smoothing = true;
			
			_notifier = new Observable(this);
		}
		
		private function connected(evt:AkamaiNotificationEvent):void {
			_ak.removeEventListener(AkamaiNotificationEvent.CONNECTED,connected);
			_ak.removeEventListener(AkamaiErrorEvent.ERROR,connectError);
			_nc = _ak.netConnection;
			connectStream();
		}
		
		private function connectError(evt:AkamaiErrorEvent):void {
			_ak.removeEventListener(AkamaiNotificationEvent.CONNECTED,connected);
			_ak.removeEventListener(AkamaiErrorEvent.ERROR,connectError);
			
			notifyObservers(VideoSourceConstants.ERROR,{code:"NetConnection.Error",msg:evt.errorDescription});
		}
		
		private function netStatusHandler(evt:NetStatusEvent):void {
			_log.debug("NetStatusEvent info: "+evt.info.code);
			notifyObservers(VideoSourceConstants.STATUS_CHANGE,{code:evt.info.code});
            switch (evt.info.code) {
                case "NetConnection.Connect.Success":
                    connectStream();
                    break;
                case "NetConnection.Connect.Failed":
                case "NetConnection.Connect.Rejected": 
                case "NetConnection.Connect.AppShutdown":
                case "NetConnection.Connect.InvalidApp":
                case "NetConnection.Call.BadVersion":
                case "NetConnection.Call.Failed":
                case "NetConnection.Call.Prohibited":
					notifyObservers(VideoSourceConstants.ERROR,{code:evt.info.code,msg:evt.info.level});
					break;
                case "NetStream.Play.StreamNotFound":
                case "NetStream.Play.FileStructureInvalid":
                case "NetStream.Play.NoSupportedTrackFound":
                case "NetStream.Play.Failed":
                case "NetStream.Failed":
                    _log.error("netStatusHandler: Unable to locate video: ", _src);
                    if (!_isManualClose) {
                    	notifyObservers(VideoSourceConstants.ERROR,{code:"NetStream.Failed"});
                    }
                    break;
                case "NetStream.Play.InsufficientBW":
                	break;
                case "NetConnection.Connect.Closed":
                	break;
                case "NetStream.Play.Stop":
					removeEventListener(Event.ENTER_FRAME,listenForEnd);
					removeEventListener(Event.ENTER_FRAME,notifyBufferingStatus);
					break;
				case "NetStream.Play.Start":
					if (_config.hostname==null) addEventListener(Event.ENTER_FRAME,listenForEnd);
					else addEventListener(Event.ENTER_FRAME,notifyBufferingStatus);
					break;
				case "NetStream.Pause.Notify":
					break;
				case "NetStream.Unpause.Notify":
					break;
				case "NetStream.Seek.Failed":
					break;
				case "NetStream.Seek.InvalidTime":
					break;
				case "NetStream.Seek.Notify":
					_isSeeking = true;
					_buffering = true;
					notifyObservers(VideoSourceConstants.BUFFERING,{percent:bufferPercent, status:_buffering});
					break;
				case "NetStream.Buffer.Flush":
					break;
                case "NetStream.Buffer.Full":
                	_ns.bufferTime = _ht_buffer;
					_isSeeking = _buffering = false;
					notifyObservers(VideoSourceConstants.BUFFERING,{percent:bufferPercent, status:_buffering});
                	break;
                case "NetStream.Buffer.Empty":
                	_ns.bufferTime = _lt_buffer;
					_buffering = true;
					notifyObservers(VideoSourceConstants.BUFFERING,{percent:bufferPercent, status:_buffering});
                	break;
            }
            
            _isManualClose = false;
        }

		private function connectStream():void {
            _log.debug("connectStream()");
            _ns = new NetStream(_nc);
            _ns.bufferTime = _lt_buffer;
			_ns.addEventListener(NetStatusEvent.NET_STATUS, netStatusHandler);
			_ns.addEventListener(AsyncErrorEvent.ASYNC_ERROR, asyncErrorHandler);
			_ns.addEventListener(SecurityErrorEvent.SECURITY_ERROR, securityErrorHandler);
			_ns.client = this;
			notifyObservers(VideoSourceConstants.STREAM_READY);
        }
        
		private function progressHandler(evt:Event):void {
		  	var perc:Number = _ns.bytesLoaded/_ns.bytesTotal;
			notifyObservers(VideoSourceConstants.LOAD_PROGRESS,{percent:perc});
		  	
		  	if (perc==1) {
		  		removeEventListener(Event.ENTER_FRAME,progressHandler);
		  		return;
		  	}
		}
		
		private function listenForEnd(evt:Event):void {
			if (int(Math.round(currTime)) == int(_videoDur) && int(_videoDur)!=0 && _config.hostname==null) {
				_isPlaying = false;
				_isPaused = false;
				notifyObservers(VideoSourceConstants.PLAYBACK_COMPLETE);
				removeEventListener(Event.ENTER_FRAME,listenForEnd);
			} else {
				notifyBufferingStatus();
			}
		}
		
		private function notifyBufferingStatus(evt:Event=null):void {
			var perc:Number = bufferPercent;
			
			switch (_config.hostname) {
				case null:
					if (_ns.bufferLength<_ns.bufferTime && _videoDur==-1) { //initial buffer
						_buffering = true;
						notifyObservers(VideoSourceConstants.BUFFERING,{percent:perc, status:_buffering});
					} else if (_ns.bufferLength<_ns.bufferTime && _ns.bufferTime==_ht_buffer) { //buffer because of seek to load point
						var avg:Number = NumberUtil.rollingAverage(_ns.time);
						if (NumberUtil.range(_ns.time-.01,_ns.time+.01,avg)) {
							_buffering = true;
							notifyObservers(VideoSourceConstants.BUFFERING,{percent:perc, status:_buffering});
						}
					} else {
						_buffering = false;
					}
					break;
				default:
					if (_isSeeking && _buffering) {
						notifyObservers(VideoSourceConstants.BUFFERING,{percent:perc, status:_buffering});
					} 
					break;
			}
		}
		
		final public function onBWDone():void{}
        
		final public function onPlayStatus(info:Object):void {
	        _log.debug("onPlayStatus: " + info.code);
	        if (info.code == "NetStream.Play.Complete") {
	        	_isPlaying = false;
				_isPaused = false;
				notifyObservers(VideoSourceConstants.PLAYBACK_COMPLETE);
	        	removeEventListener(Event.ENTER_FRAME,listenForEnd);
				removeEventListener(Event.ENTER_FRAME,notifyBufferingStatus);
	        }
	    }
        
		final public function onMetaData(info:Object):void {
	        _log.debug("metadata: duration=" + info.duration + " width=" + info.width + " height=" + info.height + " framerate=" + info.videoframerate);
			_videoDur = Number(info.duration);
			notifyObservers(VideoSourceConstants.META_DATA,info);
	        if (_config.hostname==null) addEventListener(Event.ENTER_FRAME,progressHandler);
	    }
	    
		final public function onCuePoint(info:Object):void {
	        _log.debug("cuepoint: time=" + info.time + " name=" + info.name + " type=" + info.type);
			notifyObservers(VideoSourceConstants.CUE_POINT,info);
	    }

		private function securityErrorHandler(evt:SecurityErrorEvent):void {
            _log.debug("securityErrorHandler: " + evt);
            notifyObservers(VideoSourceConstants.ERROR,evt);
        }
        
		private function asyncErrorHandler(evt:AsyncErrorEvent):void {
            _log.debug("asyncErrorHandler: " + evt);
            notifyObservers(VideoSourceConstants.ERROR,evt);
        }
        
        private function ioErrorHandler(evt:IOErrorEvent):void {
        	_log.error("ioErrorHandler: " + evt);
        	notifyObservers(VideoSourceConstants.ERROR,evt);
        }
	}
}